São Paulo Road Network Analysis¶
Pedro Borges — pedro.borges@inpe.br
Geospatial network analysis of the road infrastructure in the City of São Paulo. This notebook presents the results of the analysis pipeline, including global network statistics, node/edge-level parameters, and spatial aggregations by OD zone, district, and subprefecture.
In [1]:
import geopandas as gpd
import matplotlib.pyplot as plt
import contextily as ctx
import pandas as pd
import re
from pathlib import Path
plt.rcParams.update({
"figure.facecolor": "#1a1a2e",
"axes.facecolor": "#1a1a2e",
"text.color": "white",
"axes.labelcolor": "white",
"xtick.color": "white",
"ytick.color": "white",
})
DATA_DIR = Path("data/output")
CRS_WEB_MERCATOR = "EPSG:3857"
BASEMAP = ctx.providers.CartoDB.DarkMatter
def plot_choropleth(gdf, column, title, geom_type="polygon",
cmap="plasma", figsize=(12, 10), **kwargs):
"""Plot a choropleth map with a dark basemap."""
fig, ax = plt.subplots(figsize=figsize)
plot_kwargs = dict(column=column, cmap=cmap, legend=True,
legend_kwds={"shrink": 0.6, "label": column})
if geom_type == "point":
plot_kwargs.update(markersize=0.1, alpha=0.6)
elif geom_type == "line":
plot_kwargs.update(linewidth=0.15)
else:
plot_kwargs.update(alpha=0.7, edgecolor="white", linewidth=0.3)
plot_kwargs.update(kwargs)
gdf.plot(ax=ax, **plot_kwargs)
ctx.add_basemap(ax, source=BASEMAP)
ax.grid(True, alpha=0.3, color="white", linestyle="--")
ax.set_axis_off()
ax.set_title(title, fontsize=14, color="white", pad=12)
plt.tight_layout()
plt.show()
Global Network Parameters¶
In [2]:
text = (DATA_DIR / "results.txt").read_text()
params = {}
for line in text.splitlines():
match = re.match(r"^(.+?):\s+([\d.]+)", line.strip())
if match:
params[match.group(1).strip()] = match.group(2)
df_params = pd.DataFrame(
list(params.items()), columns=["Parameter", "Value"]
)
df_params
Out[2]:
| Parameter | Value | |
|---|---|---|
| 0 | Number of nodes (N) | 122456 |
| 1 | Number of edges (L) | 304027 |
| 2 | Average degree (<k>) | 4.9655 |
| 3 | Average clustering coefficient (<c>) | 0.0596 |
| 4 | Average Euclidean distance (<l_eucl>) | 92.0898 |
| 5 | Average Manhattan distance (<l_manh>) | 117.0374 |
| 6 | Average physical length (<length>) | 96.9404 |
| 7 | Topological (edges) | 114.3566 |
| 8 | Physical length (m) | 23144.3021 |
| 9 | Euclidean distance (m) | 22754.0413 |
| 10 | Manhattan distance (m) | 28499.9732 |
| 11 | Maximum Euclidean distance (max_l_eucl) | 6540.5646 |
| 12 | Maximum Manhattan distance (max_l_manh) | 8611.8547 |
| 13 | Maximum physical length (max_length) | 6833.8703 |
| 14 | Diameter (D) | 357 |
| 15 | p = 2L / N(N-1) | 0.000041 |
| 16 | k* = p(N-1) | 4.9655 |
| 17 | c* = k*/N | 0.000041 |
| 18 | l* = logN/logk* | 7.3107 |
Node Parameters¶
- k_i — Node degree: number of edges connected to each node
- b_i — Node betweenness centrality: fraction of shortest paths passing through each node
- c_i — Clustering coefficient: fraction of a node's neighbors that are also neighbors of each other
In [3]:
nodes = gpd.read_file(DATA_DIR / "nodes.gpkg").to_crs(CRS_WEB_MERCATOR)
nodes[["k_i", "c_i", "b_i", "avg_l_i"]].describe()
Out[3]:
| k_i | c_i | b_i | avg_l_i | |
|---|---|---|---|---|
| count | 122456.000000 | 122456.000000 | 122456.000000 | 122456.000000 |
| mean | 4.965490 | 0.059641 | 0.001071 | 95.989565 |
| std | 1.709278 | 0.127200 | 0.006706 | 90.543756 |
| min | 1.000000 | 0.000000 | 0.000000 | 0.000000 |
| 25% | 4.000000 | 0.000000 | 0.000020 | 54.346951 |
| 50% | 6.000000 | 0.000000 | 0.000078 | 82.972008 |
| 75% | 6.000000 | 0.000000 | 0.000329 | 116.270134 |
| max | 11.000000 | 1.000000 | 0.202894 | 6833.870270 |
In [4]:
plot_choropleth(nodes, "k_i", "Node Degree (k_i)", geom_type="point")
plot_choropleth(nodes, "b_i", "Node Betweenness Centrality (b_i)", geom_type="point")
Edge Parameters¶
- e_ij — Edge betweenness centrality: fraction of shortest paths passing through each edge
In [5]:
edges = gpd.read_file(DATA_DIR / "edges.gpkg").to_crs(CRS_WEB_MERCATOR)
plot_choropleth(edges, "e_ij", "Edge Betweenness Centrality (e_ij)", geom_type="line")
Aggregated Parameters by OD Zone¶
In [6]:
zones = gpd.read_file(DATA_DIR / "zone_summary.gpkg").to_crs(CRS_WEB_MERCATOR)
plot_choropleth(zones, "k_i_mean", "Mean Node Degree by OD Zone (k_i_mean)")
plot_choropleth(zones, "c_i_mean", "Mean Clustering Coefficient by OD Zone (c_i_mean)")
plot_choropleth(zones, "b_i_mean", "Mean Node Betweenness by OD Zone (b_i_mean)")
plot_choropleth(zones, "e_ij_mean", "Mean Edge Betweenness by OD Zone (e_ij_mean)")
Aggregated Parameters by District¶
In [7]:
districts = gpd.read_file(DATA_DIR / "district_summary.gpkg").to_crs(CRS_WEB_MERCATOR)
plot_choropleth(districts, "k_i_mean", "Mean Node Degree by District (k_i_mean)")
plot_choropleth(districts, "c_i_mean", "Mean Clustering Coefficient by District (c_i_mean)")
plot_choropleth(districts, "b_i_mean", "Mean Node Betweenness by District (b_i_mean)")
plot_choropleth(districts, "e_ij_mean", "Mean Edge Betweenness by District (e_ij_mean)")
Aggregated Parameters by Subprefecture¶
In [8]:
subpref = gpd.read_file(DATA_DIR / "subprefeitura_summary.gpkg").to_crs(CRS_WEB_MERCATOR)
plot_choropleth(subpref, "k_i_mean", "Mean Node Degree by Subprefecture (k_i_mean)")
plot_choropleth(subpref, "c_i_mean", "Mean Clustering Coefficient by Subprefecture (c_i_mean)")
plot_choropleth(subpref, "b_i_mean", "Mean Node Betweenness by Subprefecture (b_i_mean)")
plot_choropleth(subpref, "e_ij_mean", "Mean Edge Betweenness by Subprefecture (e_ij_mean)")
In [9]:
subpref_table = (
subpref
.drop(columns="geometry")
.sort_values("Subprefeitura")
.reset_index(drop=True)
.round(4)
)
with pd.option_context("display.max_rows", None):
display(subpref_table)
| Subprefeitura | k_i_mean | k_i_median | k_i_max | c_i_mean | c_i_median | c_i_max | b_i_mean | b_i_median | b_i_max | e_ij_mean | e_ij_median | e_ij_max | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Aricanduva/Formosa/Carrão | 5.1200 | 6.0 | 11 | 0.0660 | 0.0 | 1.0000 | 0.0012 | 0.0001 | 0.1356 | 0.0005 | 0.0 | 0.0759 |
| 1 | Butantã | 4.7034 | 5.0 | 10 | 0.0799 | 0.0 | 1.0000 | 0.0009 | 0.0001 | 0.1700 | 0.0004 | 0.0 | 0.1701 |
| 2 | Campo Limpo | 5.0959 | 6.0 | 10 | 0.0763 | 0.0 | 1.0000 | 0.0010 | 0.0001 | 0.1511 | 0.0004 | 0.0 | 0.1511 |
| 3 | Capela do Socorro | 5.1308 | 6.0 | 10 | 0.0643 | 0.0 | 1.0000 | 0.0015 | 0.0001 | 0.0925 | 0.0006 | 0.0 | 0.0698 |
| 4 | Casa Verde | 5.1297 | 6.0 | 8 | 0.0537 | 0.0 | 0.6667 | 0.0012 | 0.0001 | 0.1731 | 0.0005 | 0.0 | 0.1731 |
| 5 | Cidade Ademar | 5.2814 | 6.0 | 10 | 0.0558 | 0.0 | 1.0000 | 0.0006 | 0.0001 | 0.0159 | 0.0002 | 0.0 | 0.0308 |
| 6 | Cidade Tiradentes | 5.1410 | 6.0 | 8 | 0.0422 | 0.0 | 0.6667 | 0.0005 | 0.0000 | 0.0201 | 0.0002 | 0.0 | 0.0105 |
| 7 | Ermelino Matarazzo | 5.3073 | 6.0 | 10 | 0.0593 | 0.0 | 1.0000 | 0.0003 | 0.0001 | 0.0662 | 0.0001 | 0.0 | 0.0500 |
| 8 | Freguesia do Ó/Brasilândia | 5.1202 | 6.0 | 10 | 0.0635 | 0.0 | 1.0000 | 0.0007 | 0.0001 | 0.1704 | 0.0003 | 0.0 | 0.1703 |
| 9 | Guaianases | 5.3644 | 6.0 | 8 | 0.0465 | 0.0 | 1.0000 | 0.0006 | 0.0001 | 0.0206 | 0.0002 | 0.0 | 0.0144 |
| 10 | Ipiranga | 4.8174 | 5.0 | 10 | 0.0628 | 0.0 | 1.0000 | 0.0005 | 0.0001 | 0.0206 | 0.0002 | 0.0 | 0.0203 |
| 11 | Itaim Paulista | 5.5333 | 6.0 | 10 | 0.0507 | 0.0 | 1.0000 | 0.0006 | 0.0001 | 0.0243 | 0.0002 | 0.0 | 0.0168 |
| 12 | Itaquera | 5.2367 | 6.0 | 9 | 0.0505 | 0.0 | 1.0000 | 0.0013 | 0.0001 | 0.0919 | 0.0005 | 0.0 | 0.0897 |
| 13 | Jabaquara | 5.1120 | 6.0 | 8 | 0.0713 | 0.0 | 0.6667 | 0.0006 | 0.0001 | 0.0175 | 0.0002 | 0.0 | 0.0175 |
| 14 | Jaçanã/Tremembé | 5.0777 | 6.0 | 8 | 0.0441 | 0.0 | 1.0000 | 0.0004 | 0.0000 | 0.0119 | 0.0002 | 0.0 | 0.0119 |
| 15 | Lapa | 4.3191 | 4.0 | 9 | 0.0947 | 0.0 | 1.0000 | 0.0018 | 0.0001 | 0.1757 | 0.0008 | 0.0 | 0.1757 |
| 16 | M'Boi Mirim | 5.0501 | 6.0 | 10 | 0.0564 | 0.0 | 1.0000 | 0.0011 | 0.0001 | 0.1142 | 0.0004 | 0.0 | 0.1142 |
| 17 | Mooca | 4.3688 | 4.0 | 10 | 0.0576 | 0.0 | 0.6667 | 0.0017 | 0.0001 | 0.2029 | 0.0008 | 0.0 | 0.2029 |
| 18 | Parelheiros | 4.9349 | 6.0 | 10 | 0.0372 | 0.0 | 1.0000 | 0.0008 | 0.0000 | 0.0404 | 0.0003 | 0.0 | 0.0332 |
| 19 | Penha | 5.1879 | 6.0 | 10 | 0.0527 | 0.0 | 1.0000 | 0.0016 | 0.0001 | 0.1178 | 0.0006 | 0.0 | 0.1179 |
| 20 | Perus | 5.0757 | 6.0 | 8 | 0.0604 | 0.0 | 1.0000 | 0.0006 | 0.0000 | 0.0232 | 0.0002 | 0.0 | 0.0232 |
| 21 | Pinheiros | 4.0688 | 4.0 | 9 | 0.0781 | 0.0 | 0.6667 | 0.0013 | 0.0001 | 0.1595 | 0.0006 | 0.0 | 0.1594 |
| 22 | Pirituba/Jaraguá | 4.9740 | 6.0 | 10 | 0.0717 | 0.0 | 1.0000 | 0.0008 | 0.0001 | 0.1704 | 0.0003 | 0.0 | 0.1678 |
| 23 | Santana/Tucuruvi | 4.8520 | 6.0 | 10 | 0.0586 | 0.0 | 1.0000 | 0.0011 | 0.0001 | 0.1761 | 0.0005 | 0.0 | 0.1761 |
| 24 | Santo Amaro | 4.6591 | 4.0 | 10 | 0.0593 | 0.0 | 1.0000 | 0.0022 | 0.0001 | 0.1525 | 0.0009 | 0.0 | 0.1525 |
| 25 | Sapopemba | 5.1946 | 6.0 | 8 | 0.0478 | 0.0 | 1.0000 | 0.0004 | 0.0001 | 0.0084 | 0.0001 | 0.0 | 0.0044 |
| 26 | São Mateus | 5.2957 | 6.0 | 10 | 0.0516 | 0.0 | 1.0000 | 0.0007 | 0.0001 | 0.0579 | 0.0003 | 0.0 | 0.0517 |
| 27 | São Miguel Paulista | 5.5656 | 6.0 | 10 | 0.0341 | 0.0 | 1.0000 | 0.0010 | 0.0001 | 0.0659 | 0.0003 | 0.0 | 0.0500 |
| 28 | Sé | 3.9044 | 4.0 | 8 | 0.0672 | 0.0 | 0.6667 | 0.0014 | 0.0001 | 0.1955 | 0.0007 | 0.0 | 0.1746 |
| 29 | Vila Maria/Vila Guilherme | 5.0813 | 6.0 | 10 | 0.0590 | 0.0 | 0.6667 | 0.0019 | 0.0001 | 0.1533 | 0.0008 | 0.0 | 0.1533 |
| 30 | Vila Mariana | 4.4938 | 4.0 | 10 | 0.0523 | 0.0 | 0.6667 | 0.0010 | 0.0001 | 0.0391 | 0.0005 | 0.0 | 0.0391 |
| 31 | Vila Prudente | 5.0600 | 6.0 | 8 | 0.0488 | 0.0 | 1.0000 | 0.0007 | 0.0001 | 0.0205 | 0.0003 | 0.0 | 0.0204 |